package controller;

import helper.StringUtils;
import helper.Utils;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;

import model.CodeLine;
import model.Command;
import model.CommandParameter;
import model.Constant;
import model.Macro;
import model.MacroList;
import model.Memory;
import model.Reference;
import model.Register;
import model.Variable;

public class Parser {

	private int currentRegisterSize = 0;

	// Singelton!
	private static Parser instance;

	private Hashtable<String, Integer> labels;

	private Parser() {

	}

	public static Parser getParser() {
		if (instance == null) {
			instance = new Parser();
		}
		return instance;
	}

	public Collection<Register> parse(Hashtable<Integer, String> code,
			Hashtable<Integer, String> labels) {

		currentRegisterSize = 0;

		ArrayList<Register> register = new ArrayList<Register>();

		HashMap<Integer, CodeLine> codeLines = parseText(code, labels);
		this.labels = calculateLabelAsRegisterIndex(codeLines);

		for (CodeLine line : codeLines.values()) {
			currentRegisterSize = register.size();
			register.addAll(parseCodeLine(line));
		}

		return register;
	}

	private Hashtable<String, Integer> calculateLabelAsRegisterIndex(
			HashMap<Integer, CodeLine> code) {
		int i = 0;

		Hashtable<String, Integer> labels = new Hashtable<String, Integer>();
		for (Integer line : code.keySet()) {
			if (code.get(line) != null && code.get(line).getLabel() != null) {
				labels.put(code.get(line).getLabel(), new Integer(i));
			}
			if (code.get(line) != null && code.get(line) instanceof Macro) {
				i += ((Macro) code.get(line)).getCodeLine().size();
			} else {
				i++;
			}
		}
		return labels;
	}

	public List<Register> parseCodeLine(CodeLine line) {
		ArrayList<Register> register = new ArrayList<Register>();
		if (line instanceof Command) {
			register.add(command2register((Command) line));
		} else if (line instanceof Constant) {
			register.add(constant2register((Constant) line));
		} else if (line instanceof Macro) {
			register.addAll(macro2register((Macro) line));
		}
		return register;
	}

	private Register constant2register(Constant c) {
		Register register = MemoryController.buildRegister();
		register.setValue(c.getValue());
		return register;
	}

	private Register command2register(Command cmd) {
		Register register = MemoryController.buildRegister();
		cmd = resolveLabels(cmd);
		boolean[] opcode = Utils.tailorBooleanArray(Utils.int2booleanArray(cmd
				.getOpcode()), register.getOpcodeWidth());
		int paramSize;
		if (cmd.getParameterCount() > 0) {
			paramSize = (register.getWidth() - register.getOpcodeWidth())
					/ cmd.getParameterCount();
		} else {
			paramSize = register.getWidth() - register.getOpcodeWidth();
		}
		boolean[][] paramBits = new boolean[cmd.getParameterCount()][];
		for (int i = 0; i < cmd.getParameterCount(); i++) {
			Reference ref = (Reference) cmd.getCommandParameterForIndex(i);
			if (ref == null) {
				paramBits[i] = Utils.tailorBooleanArray(Utils
						.int2booleanArray(0), paramSize);
			} else {
				paramBits[i] = Utils.tailorBooleanArray(Utils
						.int2booleanArray(ref.getAddress()), paramSize);
			}
		}
		boolean[] allParams = Utils.tailorBooleanArray(Utils
				.joinBooleanArrays(paramBits), register.getWidth()
				- register.getOpcodeWidth());
		int code = Utils.booleanArray2int(Utils.joinBooleanArrays(opcode,
				allParams));
		register.setValue(code);
		return register;
	}

	private Collection<Register> macro2register(Macro macro) {
		ArrayList<Register> register = new ArrayList<Register>();
		for (CodeLine codeLine : macro.getCodeLine()) {
			if (codeLine instanceof Command) {
				Command cmd = resolveVariable((Command) codeLine, macro);
				register.add(command2register(cmd));
			} else if (codeLine instanceof Constant) {
				register.add(constant2register((Constant) codeLine));
			}
		}
		return register;
	}

	private Command resolveVariable(Command cmd, Macro macro) {
		cmd = cmd.copy();
		ArrayList<Variable> macroVariable = new ArrayList<Variable>();
		macroVariable.addAll(macro.getVariable());
		ArrayList<CommandParameter> macroParameter = new ArrayList<CommandParameter>();
		macroParameter.addAll(macro.getCommandParameter());
		ArrayList<CommandParameter> refs = new ArrayList<CommandParameter>();

		HashMap<String, Integer> labels = new HashMap<String, Integer>();
		int index = 0;
		for (CodeLine codeLine : macro.getCodeLine()) {
			if (codeLine.getLabel() != null) {
				labels.put(codeLine.getLabel(), index + currentRegisterSize);
			}
			index++;
		}

		for (CommandParameter param : cmd.getCommandParameter()) {
			if (isVariable(param)) {
				if (param != null) {
					Variable var = (Variable) param;
					for (int i = 0; i < macroVariable.size(); i++) {
						if (macroVariable.get(i).equals(var)) {
							refs.add(macroParameter.get(i));
						}
					}
				}
			} else if (isLabel(param)) {
				if (param != null) {
					Reference ref = new Reference(labels.get(((Variable) param)
							.getName()));
					refs.add(ref);
				}
			} else {
				refs.add(param);
			}
		}
		cmd.setCommandParameter(refs);
		return cmd;
	}

	private Command resolveLabels(Command cmd) {
		cmd = cmd.copy();
		ArrayList<CommandParameter> refs = new ArrayList<CommandParameter>();
		for (CommandParameter param : cmd.getCommandParameter()) {
			if (isLabel(param) && !isVariable(param)) {
				if (param != null) {
					Integer line = labels.get(((Variable) param).getName());
					if (line != null) {
						refs.add(new Reference(line));
					}
				}
			} else {
				refs.add(param);
			}
		}
		cmd.setCommandParameter(refs);
		return cmd;

	}

	private boolean isVariable(CommandParameter param) {
		return (param instanceof Variable && ((Variable) param).getName()
				.startsWith("$"));
	}

	private boolean isLabel(CommandParameter param) {
		return (param instanceof Variable && !isVariable(param));
	}

	/**
	 * Creates CodeLines from a table of Text
	 */
	public HashMap<Integer, CodeLine> parseText(
			Hashtable<Integer, String> code, Hashtable<Integer, String> labels) {
		HashMap<Integer, CodeLine> codeLines = new HashMap<Integer, CodeLine>();
		for (Integer line : code.keySet()) {
			if (code.get(line) != null) {
				codeLines.put(line, text2codeLine(code.get(line)));
			}
		}
		for (Integer line : labels.keySet()) {
			if (codeLines.get(line) != null) {
				codeLines.get(line).setLabel(labels.get(line));
			} else {
				CodeLine cl = new Constant(0);
				cl.setLabel(labels.get(line));
				codeLines.put(line, cl);
			}
		}
		return codeLines;
	}

	/**
	 * A CodeLine should have the following syntax: <br>
	 * "COMMAND Param1,Param2,Param3,.."
	 */
	private CodeLine text2codeLine(String line) {
		String[] pieces = StringUtils.trim(line).split(" ");
		if (StringUtils.isIntegral(pieces[0])) {
			return new Constant(new Integer(pieces[0]));
		}
		for (Command command : Memory.getMinCommandSet()) {
			if (command.getName().equals(pieces[0])) {
				try {
					if (pieces.length > 1) {
						String param = StringUtils.trim(line).substring(
								StringUtils.trim(line).indexOf(' ') + 1);
						return parseCommand(command.getClass(), param
								.split(" "));
					} else {
						return parseCommand(command.getClass(), new String[] {});
					}
				} catch (Exception e) {
					e.printStackTrace();
					// TODO: handle
				}
			}
		}
		if (MacroList.getInstance().iterator() == null) {
			return null;
		}
		for (Macro macro : MacroList.getInstance()) {
			if (macro.getName().equals(pieces[0])) {
				ArrayList<CommandParameter> params = new ArrayList<CommandParameter>();
				if (pieces.length > 1) {
					for (int i = 1; i < pieces.length; i++) {
						if (StringUtils.isIntegral(pieces[i])) {
							params.add(new Reference(Integer
									.parseInt(pieces[i])));
						} else {
							params.add(new Variable(pieces[i]));
						}
					}
				}
				macro.setCommandParameter(params);
				return macro;
			}
		}
		return null;
	}

	private CodeLine parseCommand(Class<? extends Command> command_class,
			String... params) throws InstantiationException,
			IllegalAccessException {
		Command cmd = command_class.newInstance();
		cmd.setOpcode(Memory.getOpcodeForCommand(command_class));
		if (cmd.getParameterCount() != params.length) {
			throw new InstantiationException("Wrong parameter count");
		}
		ArrayList<CommandParameter> cmdParams = new ArrayList<CommandParameter>();
		for (String param : params) {
			if (StringUtils.isIntegral(param)) {
				cmdParams.add(new Reference(Integer.parseInt(param)));
			} else {
				cmdParams.add(new Variable(param));
			}
		}
		cmd.setCommandParameter(cmdParams);
		return cmd;
	}

}
